soft-reboot: Many changes
authorColin Walters <walters@verbum.org>
Mon, 30 Jun 2025 21:07:18 +0000 (17:07 -0400)
committerColin Walters <walters@verbum.org>
Tue, 8 Jul 2025 15:34:21 +0000 (11:34 -0400)
- Add --reboot and --reset arguments
- Don't compile on centos stream 9 (missing `open_tree` glibc wrapper)
  as the functionality isn't supported by systemd there; that said
  we should also do dynamic detection
- Fix /sysroot writability
- If we target as soft reboot a deployment
  *other* than the staged one, automatically clear the staged
  deployment as otherwise the semantics are too confusing.
- Rename the APIs so they all say `soft_reboot` and not `next_root`

Signed-off-by: Colin Walters <walters@verbum.org>
13 files changed:
apidoc/ostree-sections.txt
configure.ac
man/ostree-admin-prepare-soft-reboot.xml
src/libostree/libostree-devel.sym
src/libostree/ostree-soft-reboot.c
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/libotcore/otcore.h
src/ostree/ot-admin-builtin-prepare-soft-reboot.c
src/ostree/ot-builtin-admin.c
tests/kolainst/destructive/soft-reboot.sh

index 2ad402cb5146199e8e62821ef09837ed0e1e2e4a..a43d1079293b03e6d2ddd43898051d67fc6b2ac2 100644 (file)
@@ -591,7 +591,8 @@ ostree_sysroot_deployment_set_mutable
 ostree_sysroot_deployment_unlock
 ostree_sysroot_deployment_set_pinned
 ostree_sysroot_deployment_can_soft_reboot
-ostree_sysroot_deployment_prepare_next_root
+ostree_sysroot_deployment_set_soft_reboot
+ostree_sysroot_clear_soft_reboot
 ostree_sysroot_write_deployments
 ostree_sysroot_write_deployments_with_options
 ostree_sysroot_write_origin_file
index 5e32d3dca5ce2e382b992814e2077c23cf45d4b1..80237af00ea245080c5d5485ce9326cc63df67d4 100644 (file)
@@ -97,6 +97,12 @@ AC_CHECK_FUNCS([nanotime clock_gettime])
 AC_STRUCT_TIMEZONE
 AC_CHECK_HEADER([sys/xattr.h],,[AC_MSG_ERROR([You must have sys/xattr.h from glibc])])
 
+dnl new mount api
+AC_CHECK_FUNCS([open_tree])
+AM_CONDITIONAL([HAVE_SOFT_REBOOT], [test x$ac_cv_func_open_tree = xyes])
+AM_COND_IF([HAVE_SOFT_REBOOT],
+  [AC_DEFINE([HAVE_SOFT_REBOOT], 1, [Define if we have soft reboots])])
+
 AS_IF([test "$YACC" != "bison -y"], [AC_MSG_ERROR([bison not found but required])])
 
 AC_SUBST([LIBS_PRIVATE])
index fa34457b335bed61d2aada3c47f001b401c20569..f6b02211ba05678b4cc4114c86e0617c4a11eb97 100644 (file)
@@ -33,13 +33,41 @@ SPDX-License-Identifier: LGPL-2.0+
         <title>Description</title>
 
         <para>
-            Prepare the deployment at INDEX for a systemd soft reboot. INDEX must be in range and not reference the currently booted deployment.
-            It is recommended to immediately follow this with an involcation of <command>systemctl soft-reboot</command>.
+            Prepare (or unset) the deployment at INDEX for a systemd soft reboot by mounting <literal>/run/nextroot</literal>.
+            INDEX must be in range and not reference the currently booted deployment.
         </para>
 
         <para>
-            It is not supported to soft reboot into a deployment with a different kernel than the booted one.
+            It is not supported to soft reboot into a deployment with a different kernel state (including initramfs) than the booted one.
         </para>
+
+        <para>
+            If a soft reboot is initiated for a deployment that is not staged, while a staged deployment is active, the staged
+            deployment will be automatically cleared.
+        </para>
+    </refsect1>
+
+    <refsect1>
+        <title>Options</title>
+
+        <variablelist>
+            <varlistentry>
+                <term><option>--reboot</option></term>
+
+                <listitem><para>
+                    Initiate a soft reboot.
+                </para></listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><option>--reset</option></term>
+
+                <listitem><para>
+                    Clear a pending soft reboot state instead of initializing one. When this is provided,
+                    a deployment index is not required.
+                </para></listitem>
+            </varlistentry>
+        </variablelist>
     </refsect1>
 
   <refsect1>
index 4a46a0b399876aaf3f51fc1f14ddffe1c45ae0f8..610a36b7e990e4aa95edf84062c20b8357c6b92d 100644 (file)
@@ -34,5 +34,6 @@ LIBOSTREE_2025.3 {
 global:
   ostree_deployment_is_soft_reboot_target;
   ostree_sysroot_deployment_can_soft_reboot;
-  ostree_sysroot_deployment_prepare_next_root;
+  ostree_sysroot_deployment_set_soft_reboot;
+  ostree_sysroot_clear_soft_reboot;
 } LIBOSTREE_2025.2;
index 08fc909bda04fe4cdaca48ccc02bf0d726eff46d..e44db255b0ccc3c441dfe933fb85693e8f5cbfb6 100644 (file)
@@ -42,6 +42,7 @@
 gboolean
 _ostree_prepare_soft_reboot (GError **error)
 {
+#ifdef HAVE_SOFT_REBOOT
   const char *sysroot_path = "/sysroot";
   const char *target_deployment = ".";
 
@@ -81,10 +82,24 @@ _ostree_prepare_soft_reboot (GError **error)
   if (!otcore_mount_etc (config, &metadata_builder, OTCORE_RUN_NEXTROOT, error))
     return FALSE;
 
-  // Note we should have inherited the readonly sysroot
-  g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL);
-  if (mount (sysroot_path, target_sysroot, NULL, MS_BIND | MS_SILENT, NULL) < 0)
-    return glnx_throw_errno_prefix (error, "failed to bind mount sysroot");
+  // And set up /sysroot. Here since we hardcode composefs, we also hardcode
+  // having a read-only /sysroot.
+  g_variant_builder_add (&metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_SYSROOT_RO,
+                         g_variant_new_boolean (true));
+  {
+    struct mount_attr attr = { .attr_set = MOUNT_ATTR_RDONLY };
+    glnx_autofd int sysroot_fd
+        = open_tree (AT_FDCWD, sysroot_path, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+    if (sysroot_fd < 0)
+      return glnx_throw_errno_prefix (error, "open_tree(%s)", sysroot_path);
+    if (mount_setattr (sysroot_fd, "", AT_EMPTY_PATH, &attr, sizeof (struct mount_attr)) < 0)
+      return glnx_throw_errno_prefix (error, "syscall(mount_setattr) of sysroot");
+    g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL);
+    if (move_mount (sysroot_fd, "", -1, target_sysroot, MOVE_MOUNT_F_EMPTY_PATH) < 0)
+      return glnx_throw_errno_prefix (error, "syscall(move_mount) of sysroot");
+
+    g_debug ("initialized /sysroot");
+  }
 
   /* This can be used by other things to signal ostree is in use */
   {
@@ -96,4 +111,7 @@ _ostree_prepare_soft_reboot (GError **error)
   }
 
   return TRUE;
+#else
+  return glnx_throw (error, "soft reboot not supported");
+#endif
 }
index 9c7a19870a646d847d92654bcfc50c2db7d2afb2..98d296a2900d97d61c9fe8e8185ac6d01dba9992 100644 (file)
@@ -66,6 +66,8 @@
  */
 #define EARLY_PRUNE_SAFETY_MARGIN_SIZE (1 << 20) /* 1 MB */
 
+static void impl_clear_soft_reboot (void);
+
 /*
  * Like symlinkat() but overwrites (atomically) an existing
  * symlink.
@@ -2881,6 +2883,22 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, GPtrArray *n
     }
   const guint nonstaged_current_len = self->deployments->len - (self->staged_deployment ? 1 : 0);
 
+  gboolean removed_soft_reboot_target = (self->soft_reboot_target_deployment != NULL);
+  for (guint i = 0; i < new_deployments->len; i++)
+    {
+      OstreeDeployment *deployment = new_deployments->pdata[i];
+      if (ostree_deployment_is_soft_reboot_target (deployment))
+        {
+          removed_soft_reboot_target = FALSE;
+          break;
+        }
+    }
+  if (removed_soft_reboot_target)
+    {
+      g_debug ("Removing soft reboot target");
+      impl_clear_soft_reboot ();
+    }
+
   /* Assign a bootserial to each new deployment.
    */
   assign_bootserials (new_deployments);
@@ -3756,6 +3774,25 @@ ostree_sysroot_stage_tree (OstreeSysroot *self, const char *osname, const char *
                                                  &opts, out_new_deployment, cancellable, error);
 }
 
+/* Ensure ostree-finalize-staged.service is started */
+gboolean
+_ostree_sysroot_ensure_finalize_staged_service (GError **error)
+{
+  // The service which performs finalization
+  const char *svc = "ostree-finalize-staged.service";
+
+  const char *const systemctl_argv[] = { "systemctl", "start", "--quiet", svc, NULL };
+  int estatus;
+  if (!g_spawn_sync (NULL, (char **)systemctl_argv, NULL,
+                     G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL,
+                     &estatus, error))
+    return FALSE;
+  if (!g_spawn_check_exit_status (estatus, error))
+    return glnx_prefix_error (error, "Failed to start %s", svc);
+
+  return TRUE;
+}
+
 /**
  * ostree_sysroot_stage_tree_with_options:
  * @self: Sysroot
@@ -3782,8 +3819,6 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname,
                                         GCancellable *cancellable, GError **error)
 {
   GLNX_AUTO_PREFIX_ERROR ("Staging deployment", error);
-  // The service which performs finalization
-  const char *svc = "ostree-finalize-staged.service";
 
   if (!_ostree_sysroot_ensure_writable (self, error))
     return FALSE;
@@ -3792,14 +3827,12 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname,
   if (booted_deployment == NULL)
     return glnx_prefix_error (error, "Cannot stage deployment");
 
-  const char *const systemctl_argv[] = { "systemctl", "start", "--quiet", svc, NULL };
-  int estatus;
-  if (!g_spawn_sync (NULL, (char **)systemctl_argv, NULL,
-                     G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL,
-                     &estatus, error))
+  // Staging always resets soft reboot state by default
+  if (!ostree_sysroot_clear_soft_reboot (self, cancellable, error))
+    return FALSE;
+
+  if (!_ostree_sysroot_ensure_finalize_staged_service (error))
     return FALSE;
-  if (!g_spawn_check_exit_status (estatus, error))
-    return glnx_prefix_error (error, "Failed to start %s", svc);
 
   g_autoptr (OstreeDeployment) deployment = NULL;
   if (!sysroot_initialize_deployment (self, osname, revision, origin, opts, &deployment,
@@ -3953,21 +3986,55 @@ ostree_sysroot_change_finalization (OstreeSysroot *self, OstreeDeployment *deplo
   return TRUE;
 }
 
+struct PrepareRootChildSetupContext
+{
+  const char *deployment_path;
+  int rootns_fd;
+};
+
+static inline void
+prepare_root_child_setup (gpointer data)
+{
+  struct PrepareRootChildSetupContext *ctx = data;
+  // Enter the root namespace first to escape the overlayfs context
+  int rc = setns (ctx->rootns_fd, CLONE_NEWNS);
+  if (rc < 0)
+    err (1, "setns");
+  // Then change to the deployment directory in the root namespace
+  rc = chdir (ctx->deployment_path);
+  if (rc < 0)
+    err (1, "chdir");
+}
+
+static gboolean _ostree_sysroot_finalize_impl_staged_deployment (OstreeSysroot *self,
+                                                                 GCancellable *cancellable,
+                                                                 GError **error);
+
 /* Invoked at shutdown time by ostree-finalize-staged.service */
 static gboolean
 _ostree_sysroot_finalize_staged_inner (OstreeSysroot *self, GCancellable *cancellable,
                                        GError **error)
 {
-  /* It's totally fine if there's no staged deployment; perhaps down the line
-   * though we could teach the ostree cmdline to tell systemd to activate the
-   * service when a staged deployment is created.
-   */
-  if (!self->staged_deployment)
+  /* Check if we have anythign to do */
+  if (!self->staged_deployment && !self->soft_reboot_target_deployment)
     {
-      ot_journal_print (LOG_INFO, "No deployment staged for finalization");
+      ot_journal_print (LOG_INFO, "No deployment staged for finalization or soft reboot");
       return TRUE;
     }
 
+  if (!_ostree_sysroot_finalize_impl_staged_deployment (self, cancellable, error))
+    return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+_ostree_sysroot_finalize_impl_staged_deployment (OstreeSysroot *self, GCancellable *cancellable,
+                                                 GError **error)
+{
+  if (!self->staged_deployment)
+    return TRUE;
+
   /* Check if finalization is locked. */
   gboolean locked = false;
   (void)g_variant_lookup (self->staged_deployment_data, _OSTREE_SYSROOT_STAGED_KEY_LOCKED, "b",
@@ -4285,26 +4352,6 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *de
   return TRUE;
 }
 
-struct PrepareRootChildSetupContext
-{
-  const char *deployment_path;
-  int rootns_fd;
-};
-
-static inline void
-prepare_root_child_setup (gpointer data)
-{
-  struct PrepareRootChildSetupContext *ctx = data;
-  // Enter the root namespace first to escape the overlayfs context
-  int rc = setns (ctx->rootns_fd, CLONE_NEWNS);
-  if (rc < 0)
-    err (1, "setns");
-  // Then change to the deployment directory in the root namespace
-  rc = chdir (ctx->deployment_path);
-  if (rc < 0)
-    err (1, "chdir");
-}
-
 /**
  * ostree_sysroot_deployment_can_soft_reboot:
  * @self: The #OstreeSysroot object.
@@ -4331,8 +4378,21 @@ ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment
   return false;
 }
 
+static void
+impl_clear_soft_reboot (void)
+{
+  int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL;
+  // If we failed to initialize the soft reboot, ensure that we've unwound any mounts
+  const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL };
+  // To aid debugging allow skipping cleanup on failure
+  if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP"))
+    g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL);
+
+  (void)unlinkat (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, 0);
+}
+
 /**
- * ostree_sysroot_deployment_prepare_next_root
+ * ostree_sysroot_deployment_set_soft_reboot:
  * @self: Sysroot
  * @deployment: Deployment to prepare /run/nextroot
  * @allow_kernel_skew: Continue even if there is a kernel mismatch
@@ -4345,30 +4405,40 @@ ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment
  * Since: TODO
  */
 gboolean
-ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployment *deployment,
-                                             gboolean allow_kernel_skew, GCancellable *cancellable,
-                                             GError **error)
+ostree_sysroot_deployment_set_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment,
+                                           gboolean allow_kernel_skew, GCancellable *cancellable,
+                                           GError **error)
 {
+#ifdef HAVE_SOFT_REBOOT
   GLNX_AUTO_PREFIX_ERROR ("Preparing /run/nextroot for a soft-reboot", error);
 
   if (!ostree_sysroot_deployment_can_soft_reboot (self, deployment) && !allow_kernel_skew)
-    {
-      return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel");
-    }
+    return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel state");
 
-  // For targeting a staged deployment, we finalize now to ensure that we have /etc
-  if (ostree_deployment_is_staged (deployment))
+  if (!_ostree_sysroot_ensure_finalize_staged_service (error))
+    return FALSE;
+
+  // Preparing a soft reboot while a staged deployment is active, but targeting
+  // a deployment other than the staged one will unset the staged state.
+  if (self->staged_deployment != NULL && deployment != self->staged_deployment)
     {
-      if (!_ostree_sysroot_finalize_staged (self, NULL, error))
+      g_autoptr (GPtrArray) current_deployments = ostree_sysroot_get_deployments (self);
+      g_assert (current_deployments->len > 0);
+      g_assert (current_deployments->pdata[0] == self->staged_deployment);
+      g_ptr_array_remove_index (current_deployments, 0);
+      if (!ostree_sysroot_write_deployments (self, current_deployments, cancellable, error))
         return FALSE;
     }
 
   g_autofree char *deployment_relpath = ostree_sysroot_get_deployment_dirpath (self, deployment);
+  // We only support queuing a soft reboot from a booted host right now, so ignore self->sysroot_fd
+  g_assert (self->booted_deployment);
   g_autofree char *deployment_fullpath = g_build_filename ("/sysroot", deployment_relpath, NULL);
   gint estatus;
 
   const char *argv[] = { "ostree", "admin", "impl-prepare-soft-reboot", NULL };
 
+  // The outer CLI entered a mount namespace; escape it
   glnx_autofd int rootns_fd = -1;
   if (!glnx_openat_rdonly (AT_FDCWD, "/proc/1/ns/mnt", TRUE, &rootns_fd, error))
     return FALSE;
@@ -4384,18 +4454,41 @@ ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployme
 
   if (!g_spawn_check_exit_status (estatus, error))
     {
-      int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL;
-      // If we failed to initialize the soft reboot, ensure that we've unwound any mounts
-      const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL };
-      // To aid debugging allow skipping cleanup on failure
-      if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP"))
-        g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL);
+      impl_clear_soft_reboot ();
       return FALSE;
     }
 
-  ot_journal_print (LOG_INFO, "Set up soft reboot at /run/nextroot");
+  g_debug ("Soft reboot setup complete");
 
-  return TRUE;
+  // Last step
+  return write_deployments_finish (self, cancellable, error);
+#else
+  return glnx_throw (error, "soft reboot not supported");
+#endif
+}
+
+/**
+ * ostree_sysroot_clear_soft_reboot:
+ * @self: Sysroot
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * If there is a soft reboot queued in /run/nextroot, clear it. If one
+ * is not queued, this function successfully does nothing.
+ *
+ * Since: TODO
+ */
+gboolean
+ostree_sysroot_clear_soft_reboot (OstreeSysroot *self, GCancellable *cancellable, GError **error)
+{
+  if (!self->soft_reboot_target_deployment)
+    return TRUE;
+
+  impl_clear_soft_reboot ();
+
+  g_debug ("Cleared soft reboot queued state");
+
+  return write_deployments_finish (self, cancellable, error);
 }
 
 /**
index 1ca12c268b427431fab02d4e5fc4ac5cec15a1ab..f5c708726548648a7971746962e0e2bcb3c3a2c8 100644 (file)
@@ -83,7 +83,7 @@ struct OstreeSysroot
   dev_t root_device;
   ino_t root_inode;
   /* The device inode for a queued soft reboot deployment */
-  gboolean have_nextroot;
+  gboolean expecting_nextroot;
   dev_t nextroot_device;
   ino_t nextroot_inode;
 
@@ -95,6 +95,7 @@ struct OstreeSysroot
   int bootversion;
   int subbootversion;
   OstreeDeployment *booted_deployment;
+  OstreeDeployment *soft_reboot_target_deployment;
   OstreeDeployment *staged_deployment;
   GVariant *staged_deployment_data;
   // True if loaded_ts is initialized
@@ -157,6 +158,8 @@ void _ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment,
 
 gboolean _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error);
 
+gboolean _ostree_sysroot_ensure_finalize_staged_service (GError **error);
+
 gboolean _ostree_sysroot_finalize_staged (OstreeSysroot *self, GCancellable *cancellable,
                                           GError **error);
 gboolean _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable,
index 3e936174b68862e695330f6dc05837ab67d5a724..40498411fa467f746f989869134bca572ede9469 100644 (file)
@@ -961,7 +961,9 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment *
   ret_deployment->device = stbuf.st_dev;
   ret_deployment->inode = stbuf.st_ino;
 
-  g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked);
+  g_debug ("Deployment %s.%d unlocked=%d dev=%" G_GUINT64_FORMAT " ino=%" G_GUINT64_FORMAT,
+           treecsum, deployserial, ret_deployment->unlocked, ret_deployment->device,
+           ret_deployment->inode);
 
   if (is_booted_deployment)
     self->booted_deployment = g_object_ref (ret_deployment);
@@ -1224,14 +1226,18 @@ _ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error)
 {
   GLNX_AUTO_PREFIX_ERROR ("Loading nextroot", error);
   // Reset state
-  self->have_nextroot = FALSE;
+  self->expecting_nextroot = FALSE;
+  g_clear_object (&self->soft_reboot_target_deployment);
 
   glnx_autofd int fd = -1;
   if (!ot_openat_ignore_enoent (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, &fd, error))
     return FALSE;
   // If there's no such file, we're done
   if (fd == -1)
-    return TRUE;
+    {
+      g_debug ("No %s", OTCORE_RUN_NEXTROOT_BOOTED);
+      return TRUE;
+    }
 
   // Parse the GVariant metadata from this; search for OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO
   // to find similar code.
@@ -1246,10 +1252,12 @@ _ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error)
   if (!backing_devino)
     return glnx_throw (error, "Missing %s key in %s", OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO,
                        OTCORE_RUN_NEXTROOT_BOOTED);
-
   // Load the device/inode, and we're done
   g_variant_get (backing_devino, "(tt)", &backing_dev, &backing_ino);
-  self->have_nextroot = TRUE;
+  g_debug ("Expecting nextroot dev %" G_GUINT64_FORMAT " ino %" G_GUINT64_FORMAT, backing_dev,
+           backing_ino);
+
+  self->expecting_nextroot = TRUE;
   self->nextroot_device = (dev_t)backing_dev;
   self->nextroot_inode = (ino_t)backing_ino;
 
@@ -1327,19 +1335,30 @@ sysroot_load_from_bootloader_configs (OstreeSysroot *self, GCancellable *cancell
     g_ptr_array_insert (deployments, 0, g_object_ref (self->staged_deployment));
 
   /* Synchronize internal state now that we've loaded all deployments */
+  g_debug ("expecting nextroot: %d", self->expecting_nextroot);
   for (guint i = 0; i < deployments->len; i++)
     {
       OstreeDeployment *deployment = deployments->pdata[i];
       ostree_deployment_set_index (deployment, i);
 
       g_assert (deployment->devino_initialized);
-      if (self->have_nextroot && deployment->device == self->nextroot_device
+      if (self->expecting_nextroot && deployment->device == self->nextroot_device
           && deployment->inode == self->nextroot_inode)
         {
           deployment->soft_reboot_target = TRUE;
+          g_assert (!self->soft_reboot_target_deployment);
+          self->soft_reboot_target_deployment = g_object_ref (deployment);
         }
     }
 
+  if (self->expecting_nextroot && !self->soft_reboot_target_deployment)
+    {
+      g_debug ("Soft reboot target not found");
+      if (!glnx_unlinkat (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, 0, error))
+        return FALSE;
+      self->expecting_nextroot = FALSE;
+    }
+
   /* Determine whether we're "physical" or not, the first time we load deployments */
   if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED)
     {
index 327c6d4515174c54c49de9943df3fa9823817231..c1fb1482260a0a13879a9acb7b671e99a77e573b 100644 (file)
@@ -271,11 +271,15 @@ gboolean ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, const c
 _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self,
                                                                    OstreeDeployment *deployment);
 
-_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self,
-                                                                     OstreeDeployment *deployment,
-                                                                     gboolean allow_kernel_skew,
-                                                                     GCancellable *cancellable,
-                                                                     GError **error);
+_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_set_soft_reboot (OstreeSysroot *self,
+                                                                   OstreeDeployment *deployment,
+                                                                   gboolean allow_kernel_skew,
+                                                                   GCancellable *cancellable,
+                                                                   GError **error);
+
+_OSTREE_PUBLIC gboolean ostree_sysroot_clear_soft_reboot (OstreeSysroot *self,
+                                                          GCancellable *cancellable,
+                                                          GError **error);
 
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_deployment_kexec_load (OstreeSysroot *self, OstreeDeployment *deployment,
index 24b4d6cbf1a70a4abe3e8afe692541944f02e2d0..19348be7a44c8a57c483ebba1b64bbb5ae810334 100644 (file)
@@ -140,6 +140,8 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder,
 // from ostree-prepare-root.
 #define OTCORE_RUN_BOOTED "/run/ostree-booted"
 // Written by ostree-soft-reboot.c with metadata about /run/nextroot
+// that is then processed by ostree-boot-complete.c and turned into
+// the canonical /run/ostree-booted.
 #define OTCORE_RUN_NEXTROOT_BOOTED "/run/ostree/nextroot-booted"
 // This key will be present if composefs was successfully used.
 #define OTCORE_RUN_BOOTED_KEY_COMPOSEFS "composefs"
index 3151f6e26c1f22bf975e73acddf73d3f61134f09..c8ccda7b74bd486091488347496439f6da008450 100644 (file)
 #include "ot-admin-functions.h"
 #include "otutil.h"
 
-static GOptionEntry options[] = { { NULL } };
+static gboolean opt_reboot;
+static gboolean opt_reset;
+
+static GOptionEntry options[]
+    = { { "reboot", 0, 0, G_OPTION_ARG_NONE, &opt_reboot, "Initiate a soft reboot on success",
+          NULL },
+        { "reset", 0, 0, G_OPTION_ARG_NONE, &opt_reset, "Undo queued soft reboot state", NULL },
+        { NULL } };
 
 gboolean
 ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvocation *invocation,
@@ -40,6 +47,9 @@ ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvoca
                                           cancellable, error))
     return FALSE;
 
+  if (opt_reset)
+    return ostree_sysroot_clear_soft_reboot (sysroot, cancellable, error);
+
   if (argc < 2)
     {
       ot_util_usage_error (context, "INDEX must be specified", error);
@@ -68,9 +78,15 @@ ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvoca
       return FALSE;
     }
 
-  if (!ostree_sysroot_deployment_prepare_next_root (sysroot, target_deployment, FALSE, cancellable,
-                                                    error))
+  if (!ostree_sysroot_deployment_set_soft_reboot (sysroot, target_deployment, FALSE, cancellable,
+                                                  error))
     return FALSE;
 
+  if (opt_reboot)
+    {
+      execlp ("systemctl", "systemctl", "soft-reboot", NULL);
+      return glnx_throw_errno_prefix (error, "exec(systemctl soft-reboot)");
+    }
+
   return TRUE;
 }
index a89afb2bea8f0acd9835f5dfba156a0c634aeece..4df9e6908d79d52764cf3d4ed1a0dc41495e5720 100644 (file)
@@ -42,8 +42,12 @@ static OstreeCommand admin_subcommands[] = {
     "Change the finalization locking state of the staged deployment" },
   { "boot-complete", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
     ot_admin_builtin_boot_complete, "Internal command to run at boot after an update was applied" },
+#ifdef HAVE_SOFT_REBOOT
   { "impl-prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
     ot_admin_builtin_impl_prepare_soft_reboot, "Internal command to prepare soft reboot" },
+  { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot,
+    "Prepare deployment for soft-reboot" },
+#endif
   { "state-overlay", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
     ot_admin_builtin_state_overlay, "Internal command to assemble a state overlay" },
   { "init-fs", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_init_fs,
@@ -59,8 +63,6 @@ static OstreeCommand admin_subcommands[] = {
     "rollback strings" },
   { "post-copy", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_post_copy,
     "Update the repo and deployments as needed after a copy" },
-  { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot,
-    "Prepare deployment for soft-reboot" },
   { "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin,
     "Set Origin and create a new origin file" },
   { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" },
index 8436525893ea7569ab6f9144fcb6287baa04ac2b..3ba99e2f1a8f3accaa82729f0eda56aec361f158 100755 (executable)
@@ -3,15 +3,30 @@ set -xeuo pipefail
 
 . ${KOLA_EXT_DATA}/libinsttest.sh
 
-require_writable_sysroot
 prepare_tmpdir
 
+echo "testing boot=${AUTOPKGTEST_REBOOT_MARK:-}"
+
+# Print this by default on each boot
+ostree admin status
+
+# Verify /sysroot readonly by default on each boot
+test '!' -w /sysroot
+findmnt -J /sysroot > findmnt.json
+assert_jq findmnt.json '.filesystems[0].options | contains("ro")'
+# But mount it writable now so we can make test commits conveniently
+require_writable_sysroot
+
+assert_soft_reboot_count() {
+  assert_streq $(systemctl show -P SoftRebootsCount) $1
+}
+
 case "${AUTOPKGTEST_REBOOT_MARK:-}" in
   "")
   # xref https://github.com/coreos/coreos-assembler/pull/2814
   systemctl mask --now zincati
 
-  assert_streq $(systemctl show -P SoftRebootsCount) 0
+  assert_soft_reboot_count 0
   assert_status_jq '.deployments[0].pending | not' '.deployments[0].["soft-reboot-target"] | not'
 
   # Create a synthetic commit for upgrade
@@ -23,7 +38,7 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in
   # Deploy the new commit normally first
   ostree admin deploy --stage soft-reboot-test
 
-  assert_status_jq '.deployments[0].pending' '.deployments[0].["soft-reboot-target"] | not'
+  assert_status_jq '.deployments[0].staged' '.deployments[0].["soft-reboot-target"] | not'
 
   # Test prepare-soft-reboot command
   echo "Testing prepare-soft-reboot..."
@@ -31,20 +46,20 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in
 
   # Test human readable format
   ostree admin status > status.txt
-  assert_file_has_content_literal status.txt '(pending) (soft-reboot)'
+  assert_file_has_content_literal status.txt '(staged) (soft-reboot)'
 
   # And via JSON
   assert_status_jq '.deployments[0].pending' '.deployments[0].["soft-reboot-target"]'
 
-  # Verify the internal state file
   test -f /run/ostree/nextroot-booted
+  mountpoint /run/nextroot
   
   /tmp/autopkgtest-soft-reboot "2"
   ;;
   "2")
   # After soft reboot, verify we're running the new deployment
   echo "Verifying post-soft-reboot state..."
-  assert_streq $(systemctl show -P SoftRebootsCount) 1
+  assert_soft_reboot_count 1
   
   expected_commit=$(ostree rev-parse soft-reboot-test)
   
@@ -58,10 +73,74 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in
   test -f /etc/new-file-for-soft-reboot
   test -f /usr/share/test-file-for-soft-reboot
   
-  # Verify that soft-reboot-pending file is cleaned up
+  # Verify that soft-reboot state files are gone
   test '!' -f /run/ostree/nextroot-booted
   
   echo "Soft reboot test completed successfully!"
+
+  # Now soft reboot again into the rollback which is not staged,
+  # and also exercise the immediate --reboot flag.
+  touch /etc/current-contents
+  /tmp/autopkgtest-soft-reboot-prepare "3"
+  ostree admin prepare-soft-reboot --reboot 1
+  ;;
+  "3")
+  assert_soft_reboot_count 2
+
+  # Only from the first updated target
+  test '!' -f /etc/new-file-for-soft-reboot
+  test '!' -f /usr/share/test-file-for-soft-reboot
+  # And this was in the *previous* current /etc
+  test '!' -f /etc/current-contents
+
+  echo "ok verified prepare"
+
+  assert_status_jq '.deployments[0].["soft-reboot-target"] | not'
+
+  ostree admin prepare-soft-reboot 0
+  assert_status_jq '.deployments[0].["soft-reboot-target"]'
+  ostree admin prepare-soft-reboot --reset
+  assert_status_jq '.deployments[0].["soft-reboot-target"] | not'
+  test '!' -f /run/ostree/nextroot-booted
+  # Test idempotence
+  ostree admin prepare-soft-reboot --reset
+  assert_status_jq '.deployments[0].["soft-reboot-target"] | not'
+  test '!' -f /run/ostree/nextroot-booted
+
+  echo "ok soft reboot 3"
+
+  # Now, test the intersection of staged deployments and soft rebooting
+  # Create another synthetic commit
+  cd /ostree/repo/tmp
+  ostree checkout -H ${host_commit} t
+  unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/share/test-staged-2-for-soft-reboot'
+  ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test-staged-2 -I --consume t
+  newcommit=$(ostree rev-parse soft-reboot-test-staged-2)
+  ostree admin deploy --stage soft-reboot-test-staged-2
+
+  assert_status_jq '.deployments[0].staged' '.deployments[0].["soft-reboot-target"] | not' \
+                   '.deployments[1].booted | not' '.deployments[1].["soft-reboot-target"] | not' \
+                   '.deployments[2].booted' '.deployments[2].["soft-reboot-target"] | not'
+
+  # Note here we're targeting the previous booted deployment, *not* the staged
+  ostree admin prepare-soft-reboot 1
+
+  # We set up the soft reboot, but cleared the staged
+  assert_status_jq '.deployments[0].booted | not' '.deployments[0].["soft-reboot-target"]' \
+                   '.deployments[1].booted' '.deployments[1].["soft-reboot-target"] | not'
+
+  # And this one verifies we do a soft reboot by default as we've mounted /run/nextroot
+  /tmp/autopkgtest-soft-reboot-prepare "4"
+  systemctl reboot
+  ;;
+  "4")
+  assert_soft_reboot_count 3
+  # Completion of soft reboot into non-staged
+  assert_status_jq '.deployments[0].booted' '.deployments[0].["soft-reboot-target"] | not' \
+                   '.deployments[1].booted | not' '.deployments[1].["soft-reboot-target"] | not'
+  echo "ok soft reboot to non-staged"
+
+  echo "ok soft reboot all tests"
   ;;
   *) 
   fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}"